/*
 *	Copyright (c) 2009 Queensland University of Technology. All rights reserved.
 *	The QUT Bioinformatics Collection is open source software released under the 
 *	Microsoft Public License (Ms-PL): http://www.microsoft.com/opensource/licenses.mspx.
 */
#undef DebugDoLayout

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using QUT.Bio.Map2D;
using QUT.Bio.Util;

// TODO: Add EdgeType and activate edge collection processing.

namespace QUT.Bio.Graphs {
	/// <summary>
	///		A Graph represents a collection of Node objects together with a matrix of
	///		Edge objects that represent directed edges between pairs of nodes.
	///		<para>
	///			In this version, each node is uniquely identified by the value of its respective
	///			_Id_ field. Rather than lists of nodes and edges, we keep lookup tables to provide 
	///			access to nodes and edges.
	///		</para>
	///		<list></list>
	/// </summary>
	/// <typeparam name="NodeType">
	///		The data type used to identify nodes in the lookup tables.
	///	</typeparam>

	public class GraphView<NodeType>
		where NodeType : IComparable<NodeType>, IEquatable<NodeType> {
		private Map2DCanvas canvas = new Map2DCanvas();

		#region Constructor

		/// <summary>
		/// Initialises a graph viewer.
		/// </summary>

		public GraphView () {
			nodes.CollectionChanged += NodesChanged;
			nodeViewIndexer = new NodeViewIndexer( this );
		}
		#endregion

		#region Property: Adapter

		/// <summary>
		/// Get or set the adapter; when adapter changes we recreate the
		/// entire view.
		/// </summary>

		public IGraphAdapter<NodeType> Adapter {
			get {
				return adapter;
			}
			set {
				if ( adapter != value ) {
					adapter = value;
					RethinkDisplay( true );
				}
			}
		}

		private IGraphAdapter<NodeType> adapter;

		#endregion

		#region Property: Canvas
		/// <summary>
		/// Gets a reference to the encapsulated Canvas.
		/// </summary>

		public Map2DCanvas Canvas {
			get {
				return canvas;
			}
		}
		#endregion

		private Dictionary<NodeType, XY> newCoordinates = new Dictionary<NodeType, XY>();

		#region field: nodeViews
		/// <summary>
		/// We maintain a list of NodeView objects, each of which represents one
		/// node in the graph. These are used in node layout, and also allow consumers
		/// to gain direct access to the PositionedElements of the graphical representation.
		/// </summary>

		private readonly Dictionary<NodeType, NodeView<NodeType>> nodeViews = new Dictionary<NodeType, NodeView<NodeType>>();
		#endregion

		#region Indexer: NodeViews

		/// <summary>
		/// Gets an indexed, enumerable property that provides consumers with
		/// relatively safe readonly access to the node view collection.
		/// </summary>

		public ReadonlyIndexer<NodeType, NodeView<NodeType>> NodeViews {
			get {
				return nodeViewIndexer;
			}
		}

		private ReadonlyIndexer<NodeType, NodeView<NodeType>> nodeViewIndexer;
		#endregion

		#region Property: Nodes

		private readonly NodeCollection<NodeType> nodes = 
			new NodeCollection<NodeType>();

		/// <summary>
		/// Gets a reference to the collection of nodes in this graph.
		/// </summary>

		public NodeCollection<NodeType> Nodes {
			get {
				return nodes;
			}
		}
		#endregion

		#region Property: Edges

		private readonly EdgeCollection<NodeType> edges = 
			new EdgeCollection<NodeType>();

		/// <summary>
		/// Gets a reference to a connection matrix that holds the edges of the graph.
		/// </summary>

		public EdgeCollection<NodeType> Edges {
			get {
				return edges;
			}
		}
		#endregion

		/// <summary>
		/// Listener for nodes.CollectionChanged.
		/// <para>
		/// When the node collection changes we:
		/// (a) remove all node views that correspond to nodes that have been removed 
		///		from the collection; 
		///	(b) create new node views for any nodes that have been added;
		/// (c) get locations for all nodes, then animate the nodes to their new locations.
		/// </para>
		/// </summary>
		/// <param name="collectionThatsChanged">The collection that has been amended; this should be that same as nodes.</param>
		public void NodesChanged ( NodeCollection<NodeType> collectionThatsChanged ) {
			adapter.UpdateNodes( nodes );

			newCoordinates.Clear();

			foreach ( var node in nodes ) {
				newCoordinates.Add( node.Content, new XY() );
			}

			UpdateDisplay( true );
		}

		// TODO: Clean up. Too many layers between the node and the UI.
		
		/// <summary>
		/// Adds new node glyphs to the display, removes any obselete node glyphs, and
		/// moves everything to its new location. I hope.
		/// </summary>
		/// <param name="animate"></param>
		
		public void UpdateDisplay ( bool animate ) {
			List<NodeType> nodesToRemove = new List<NodeType>();

			foreach ( KeyValuePair<NodeType, NodeView<NodeType>> x in nodeViews ) {
				if ( !nodes.ContainsKey( x.Key ) ) {
					nodesToRemove.Add( x.Key );
				}
			}

			foreach ( NodeType node in nodesToRemove ) {
				canvas.Remove( nodeViews[node].PositionedElement );
				nodeViews.Remove( node );
			}

			foreach ( var node in nodes ) {
				if ( ! nodeViews.ContainsKey( node.Content ) ) {
					NodeView<NodeType> nodeView = new NodeView<NodeType>(
						node,
						adapter.GetNodeShape( node ),
						adapter.GetNodeLabel( node ),
						canvas
					);
					adapter.PrepareNodeView( nodeView );
					nodeViews.Add( node.Content, nodeView );
				}
			}

			UpdateLocations( animate );
		}

		#region Method: RethinkDisplay

		/// <summary>
		///		Rebuilds the entire display, creating a nodes afresh.
		/// </summary>
		/// <param name="animate">
		///		Set true iff you want nodes to animate to their new locations.
		/// </param>

		public void RethinkDisplay ( bool animate ) {
			foreach ( var nodeView in nodeViews.Values ) {
				canvas.Remove( nodeView.PositionedElement );
			}

			nodeViews.Clear();

			foreach ( var node in nodes ) {
				NodeView<NodeType> nodeView = new NodeView<NodeType>(
					node,
					adapter.GetNodeShape( node ),
					adapter.GetNodeLabel( node ),
					canvas
				);
				adapter.PrepareNodeView( nodeView );
				nodeViews.Add( node.Content, nodeView );
			}

			UpdateLocations( animate );
		}
		#endregion

		#region Method: UpdateLocations

		/// <summary>
		/// Calls doLayout to obtain up-to-date coordinates for each node and moves 
		/// the node views to the corresoponding locations.
		/// </summary>
		/// <param name="animate">
		///		Set this to true iff animation is desired during the update.
		/// </param>

		public void UpdateLocations ( bool animate ) {
			adapter.DoLayout( newCoordinates );

			InterpolatorList l = new InterpolatorList();

			foreach ( var nodeView in nodeViews.Values ) {
				PositionedElement e = nodeView.PositionedElement;
				XY p = newCoordinates[nodeView.Node.Content];

#if DebugDoLayout
				Debug.WriteLine( String.Format( "UpdateLocations: [{0},{1}] -> [{2},{3}]", e.X, e.Y, p.X, p.Y ) );
#endif

				if ( p.X != e.X || p.Y != e.Y ) {
					PositionInterpolator interpolator = nodeView.Interpolator;
					interpolator.CopyElementPosition();
					interpolator.X1 = p.X;
					interpolator.Y1 = p.Y;
					l.Add( interpolator );
				}
			}

			if ( animate ) {
				foreach ( PositionInterpolator interpolator in l.Items ) {
					interpolator.CopyElementPosition();
					Storyboard b = new Storyboard();
					Timeline t = CreateTimeLine( 1, 0.5, new TimeSpan( 0, 0, 0, 1, 500 ), interpolator, PositionInterpolator.TProperty );
					b.Children.Add( t );
					b.Begin();
				}
			}
			else {
				l.Parameter = 1;
			}
		}
		#endregion

		#region Method: CreateTimeline

		/// <summary>
		/// Creates a Timeline that moves a double value from it's current value to a new value.
		/// </summary>
		/// <param name="to">The desired new value of the target property.</param>
		/// <param name="accelerationPeriod">
		///		The proportion of the duration to spend accelerating. 
		///		If this is between 0 and 1 (exclusive of the endpoints),
		///		a spline animation is created.
		///		Otherwise, a fixed-speed animation is created.
		/// </param>
		/// <param name="duration">The period over which the animation will occur.</param>
		/// <param name="target">The object that will be moved.</param>
		/// <param name="propertyId">The dependency property to aniimate.</param>
		/// <returns>A Timeline that should do the job.</returns>

		public static Timeline CreateTimeLine (
			double to,
			double accelerationPeriod,
			TimeSpan duration,
			DependencyObject target,
			DependencyProperty propertyId
		) {
			Timeline timeLine;

			if ( accelerationPeriod <= 0 || accelerationPeriod >= 1 ) {
				DoubleAnimation animation = new DoubleAnimation();
				animation.From = (double) target.GetValue( propertyId );
				animation.To = to;
				timeLine = animation;
			}
			else {
				DoubleAnimationUsingKeyFrames animation = new DoubleAnimationUsingKeyFrames();
				animation.KeyFrames.Add( new SplineDoubleKeyFrame() {
					KeySpline = new KeySpline() {
						ControlPoint1 = new Point( accelerationPeriod, 0 ),
						ControlPoint2 = new Point( 1 - accelerationPeriod, 1 )
					},
					KeyTime = duration,
					Value = to
				} );
				timeLine = animation;
			}

			timeLine.Duration = duration;
			Storyboard.SetTarget( timeLine, target );
			Storyboard.SetTargetProperty( timeLine, new PropertyPath( propertyId ) );

			return timeLine;
		}
		#endregion

		#region Method: CreateTimeline

		/// <summary>
		/// Creates a Timeline that moves a double value from it's current value to a new value over a succession of steps.
		/// </summary>
		/// <param name="steps">A list of triples containing target value, spline control point and arrival time for each step.
		/// <para>step[0].First = to</para>
		/// <para>step[0].Second = spline control point</para>
		/// <para>step[0].Third = arrival time.</para>
		/// </param>
		/// <param name="target">The object that will be moved.</param>
		/// <param name="propertyId">The dependency property to aniimate.</param>
		/// <returns>A Timeline that should do the job.</returns>

		public static Timeline CreateTimeLine (
			DependencyObject target,
			DependencyProperty propertyId,
			params Triple<double, double, TimeSpan> [] steps
		) {
			if ( steps.Length == 1 ) {
				return CreateTimeLine( steps[0].First, steps[0].Second, steps[0].Third, target, propertyId );
			}
			
			DoubleAnimationUsingKeyFrames animation = new DoubleAnimationUsingKeyFrames();
			
			TimeSpan lastTime = TimeSpan.Zero;
				
			foreach ( Triple<double, double, TimeSpan> step in steps ) { 
				double to = step.First;
				double accelerationPeriod = step.Second;
				TimeSpan keyTime = step.Third;
				lastTime = keyTime;
				
				animation.KeyFrames.Add( new SplineDoubleKeyFrame {
					KeySpline = new KeySpline() {
						ControlPoint1 = new Point( accelerationPeriod, 0 ),
						ControlPoint2 = new Point( 1 - accelerationPeriod, 1 )
					},
					KeyTime = keyTime,
					Value = to
				} );
			}
			
			animation.Duration = lastTime;
			Storyboard.SetTarget( animation, target );
			Storyboard.SetTargetProperty( animation, new PropertyPath( propertyId ) );

			return animation;
		}
		#endregion

		#region Nested class: NodeViewIndexer
#pragma warning disable 1591
		private class NodeViewIndexer : ReadonlyIndexer<NodeType, NodeView<NodeType>> {
			private GraphView<NodeType> graph;

			public NodeViewIndexer ( GraphView<NodeType> graph ) {
				this.graph = graph;
			}

			public NodeView<NodeType> this[NodeType i] {
				get {
					return graph.nodeViews[i];
				}
			}

			public IEnumerator<NodeView<NodeType>> GetEnumerator () {
				foreach ( var v in graph.nodeViews.Values ) {
					yield return v;
				}
			}

			System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator () {
				foreach ( var v in graph.nodeViews.Values ) {
					yield return v;
				}
			}
		}
#pragma warning restore 1591
		#endregion
	}
}
